package a2;

import java.util.ArrayList;
import java.util.Arrays;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;

/**
 * FileSystem is a singleton class that represents a virtual system of files and
 * directories just as a hard drive or compact disk would. Has a
 * {@link Directory} that represents the root of this system. Handles the
 * "mkdir", "rm", "mv", "cp" and "echo" commands.
 * 
 * @author Dylan Seago, Tianran Liu, Hilal Dib, Paschal Oduoza
 */
public class FileSystem {

	/**
	 * {@link Directory} that represents the root of this {@link FileSystem}.
	 */
	private Directory root;

	/**
	 * Single static instance of {@link FileSystem}.
	 */
	private static FileSystem fileSystemInstance;

	/**
	 * Protected empty constructor to remove ability to instantiate.
	 */
	protected FileSystem() {
		// Protected empty constructor to remove ability to instantiate
	}

	/**
	 * If no instance of {@link FileSystem} exists, create one and return it.
	 * 
	 * @return A single instance of {@link FileSystem}
	 */
	public static FileSystem getInstance() {
		if (fileSystemInstance == null) {
			fileSystemInstance = new FileSystem();
			fileSystemInstance.clear();
		}
		return fileSystemInstance;
	}

	/**
	 * Clears the {@link FileSystem}. Erases the current root {@link Directory}
	 * and replaces it with a newly instantiated empty one.
	 */
	public void clear() {
		root = new Directory("", null);
	}

	/**
	 * Gets the root {@link Directory}
	 * 
	 * @return The root {@link Directory}.
	 */
	public Directory getRoot() {
		return root;
	}

	/**
	 * Creates a {@link Directory} at the given relative or absolute path. Any
	 * directories specified in the path that do not exist will also be created.
	 * 
	 * @param currentDir
	 *            Current working {@link Directory}
	 * @param path
	 *            Path of the {@link Directory}
	 */
	public void mkdir(Directory currentDir, String path)
			throws InvalidPathException {
		if (isValidName(path, true)) {
			if (isRelativePath(path))
				mkdir(currentDir, getSplitPath(path), path);
			else
				mkdir(root, getSplitPath(path), path);
		} else {
			throw new InvalidPathException("No special characters allowed, "
					+ path);
		}
	}

	/**
	 * Recursive helper function for {@link #mkdir(Directory, String) mkdir}.
	 */
	private void mkdir(Directory currentDir, ArrayList<String> paths,
			String path) {
		if (paths.size() > 0) {
			String dirName = paths.remove(0);
			// Relative term entered
			if (dirName.equals(".")) {
				mkdir(currentDir, paths, path);
			}
			// Parent term entered
			else if (dirName.equals("..")) {
				mkdir(currentDir.getParent(), paths, path);
			}
			// Normal term has been entered
			else {
				Item item = currentDir.getChild(dirName);
				if (item != null) {
					// Throw error if path refers to a file that exists
					if (item instanceof File)
						System.out.println("mkdir: cannot create directory '"
								+ path + "': Not a directory");
					else {
						// If item is the final directory specified
						if (paths.size() == 0)
							System.out
									.println("mkdir: cannot create directory '"
											+ path + "': Directory exists");
						// Otherwise continue creating directories at item
						else
							mkdir((Directory) item, paths, path);
					}
				}

				// If directory does not yet exist, create it and continue
				// recursively creating the directories starting at the new
				// directory
				else {
					Directory dir = new Directory(dirName, currentDir);
					currentDir.getChildren().add(dir);
					mkdir(dir, paths, path);
				}
			}
		}
	}

	/**
	 * Removes the {@link Item} at the given relative or absolute path.
	 * 
	 * @param currentDir
	 *            Current working {@link Directory}
	 * @param path
	 *            Path of the {@link Item}
	 */
	public void rm(Directory currentDir, String path)
			throws PathNotFoundException {
		Item item = findItem(currentDir, path);
		rm(item);
	}

	/**
	 * Private helper function for {@link mv} and
	 * {@link FileSystem#rm(Directory, String) rm} methods. Removes the
	 * {@link Item} from the {@link FileSystem}.
	 * 
	 * @param item
	 *            {@link Item} to be removed from the {@link FileSystem}
	 */
	private void rm(Item item) {
		item.getParent().getChildren().remove(item);
	}

	/**
	 * Moves the {@link Item} at the given {@code sourcePath} to the given
	 * {@code destPath}. Any directories in the {@code destPath} that do not
	 * exist will also be created.
	 * 
	 * @param currentDir
	 *            Current working {@link Directory}
	 * @param sourcePath
	 *            Path of the {@link Item} to be moved.
	 * @param destPath
	 *            Path where the {@link Item} is to be moved to.
	 * @throws PathNotFoundException
	 *             Thrown when sourcePath cannot be found
	 */
	public void mv(Directory currentDir, String sourcePath, String destPath)
			throws PathNotFoundException {
		cp(currentDir, sourcePath, destPath);
		rm(findItem(currentDir, sourcePath));
	}

	/**
	 * Copies the {@link Item} at the given {@code sourcePath} to the given
	 * {@code destPath}. Any directories in the {@code destPath} that do not
	 * exist will also be created.
	 * 
	 * @param currentDir
	 *            Current working {@link Directory}
	 * @param sourcePath
	 *            Path of the {@link Item} to be copied.
	 * @param destPath
	 *            Path where the {@link Item} is to be copied to.
	 * @throws PathNotFoundException
	 *             Thrown when sourcePath cannot be found
	 */
	public void cp(Directory currentDir, String sourcePath, String destPath)
			throws PathNotFoundException {
		Item source = findItem(currentDir, sourcePath);
		Item dest = findItem(currentDir, destPath);

		// Moves item to dest directory
		if (dest instanceof Directory) {
			((Directory) dest).getChildren().add(source);
		}
		// Overwrites dest File content with source File content.
		else if (source instanceof File) {
			((File) dest).setContent(((File) source).getContent());
		}
		// Prints error message if source is Directory and dest is File.
		else if (source instanceof Directory)
			System.out.println("mv: cannot overwrite non-directory '"
					+ destPath + "' with directory '" + sourcePath + "'");
	}

	/**
	 * Prints {@code input} to the console.
	 * 
	 * @param currentDir
	 *            Current working {@link Directory}.
	 * @param input
	 *            {@link String} to be output to console.
	 */
	public void echo(Directory currentDir, String input) {
		System.out.println(input);
	}

	/**
	 * Puts the given {@code input} into the {@link File} at {@code outputPath}.
	 * If the {@link File} at {@code outputPath} exists it is overwritten. If it
	 * does not exist a new {@link File} is created and written to.
	 * 
	 * @param currentDir
	 *            Current working {@link Directory}.
	 * @param input
	 *            {@link String} to be output to a {@link File}.
	 * @param outputPath
	 *            Path of the {@link File} to be output to.
	 * @param overwrite
	 *            True when the {@link File} at {@code outputPath} is to be
	 *            overwritten.
	 */
	public void echo(Directory currentDir, String input, String outputPath,
			boolean overwrite) throws InvalidPathException,
			PathNotFoundException {
		if (isValidName(outputPath, false)) {

			// Find the specified files parent
			ArrayList<String> paths = getSplitPath(outputPath);
			String fileName = paths.remove(paths.size() - 1);
			Item parent = findItem(currentDir, paths, outputPath);

			if (parent instanceof Directory) {
				Directory parentDir = (Directory) parent;
				// Get the File from parent
				Item item = parentDir.getChild(fileName);
				// If file does not exist, create a blank file
				if (item == null) {
					item = new File(fileName, "", parentDir);
					parentDir.getChildren().add(item);
				}

				if (item instanceof File) {
					if (overwrite)
						((File) item).setContent(input);
					else
						((File) item).appendContent(input);
				}
				// Specified File is a Directory, print error
				else
					System.out.println("Cannot output to file: '" + outputPath
							+ "' is a directory");
			} else
				throw new InvalidPathException(outputPath);
		} else {
			throw new InvalidPathException(outputPath);
		}
	}

	/**
	 * Checks to see if the given name for a file or directory is valid.
	 * 
	 * @param path
	 *            The user input given.
	 * @param isDir
	 *            Tells the function whether to make "." invalid or not by
	 *            identifying if the file created is a directory or file.
	 * @return True if the name is valid
	 */
	private static boolean isValidName(String path, boolean isDir) {
		String invalidChars = "!@$&*()?:[]" + "\"" + "<>'`|={}\\,;";
		char[] pathChar = path.toCharArray();
		for (int x = 0; x < pathChar.length; x++) {
			if (invalidChars.indexOf(pathChar[x]) != -1) {
				return false;
			}
			if (!isDir) {
				if ("/".indexOf(pathChar[x]) != -1) {
					return false;
				}
			} else {
				if (".".indexOf(pathChar[x]) != -1) {
					if (x < 1
							|| ("/".equals(pathChar[x - 1]) || "/"
									.equals(pathChar[x + 1]))) {
						return false;
					}
				}
			}
		}
		return true;
	}

	/**
	 * Returns true if {@code path} is relative, false if it's absolute.
	 * Absolute paths begin with '/' as the first character. Relative paths do
	 * not.
	 * 
	 * @param path
	 *            Path to check.
	 * @return True if {@code path} is relative, false if it's absolute.
	 */
	public static boolean isRelativePath(String path) {
		if (path.substring(0, 1).equals("/"))
			return false;
		return true;
	}

	/**
	 * Splits the {@code path} into an {@link ArrayList} at every '/'
	 * 
	 * @param path
	 *            Path to split into array
	 * @return Path split into an array of Strings
	 */
	public static ArrayList<String> getSplitPath(String path) {
		ArrayList<String> paths = new ArrayList<String>(Arrays.asList(path
				.split("/")));
		if (paths.size() > 0 && paths.get(0).equals(""))
			paths.remove(0);
		return paths;
	}

	/**
	 * Finds and returns the {@link Item} located at the {@code path}. If the
	 * path is relative the method will look in {@code currentDir} otherwise it
	 * will look in the {@code root} directory.
	 * 
	 * @param currentDir
	 *            Current working {@link Directory}. Used for relative paths.
	 * @param path
	 *            Location of the {@link Item}. Can be relative or absolute.
	 * @return The item located at {@code path}.
	 * @throws PathNotFoundException
	 *             Thrown when the item could not be found at the path.
	 */
	public Item findItem(Directory currentDir, String path)
			throws PathNotFoundException {
		ArrayList<String> paths = getSplitPath(path);
		return findItem(currentDir, paths, path);

	}

	/**
	 * Private helper method for {@link FileSystem#findItem(Directory, String)
	 * findItem} and {@link FileSystem#echo(Directory, String, String, boolean)
	 * echo} methods. Finds and returns the {@link Item} located at the
	 * {@code path} split into {@code paths} array at every slash. If the path
	 * is relative the method will look in {@code currentDir} otherwise it will
	 * look in the {@code root} directory.
	 * 
	 * @param currentDir
	 *            Current working {@link Directory}. Used for relative paths.
	 * @param paths
	 *            Location of the {@link Item}. {@code path} split into
	 *            {@code paths} array at every slash.
	 * @param path
	 *            Location of the {@link Item}. Can be relative or absolute.
	 * @return The item located at {@code path}.
	 * @throws PathNotFoundException
	 *             Thrown when the item could not be found at the path.
	 */
	private Item findItem(Directory currentDir, ArrayList<String> paths,
			String path) throws PathNotFoundException {
		// Sets the parent Directory to the currentDir if path is relative,
		// otherwise it is set to the root Directory.
		try {
			if (isRelativePath(path))
				return findItemByPath(currentDir, paths);
			else {
				return findItemByPath(root, paths);
			}
			// If the above throws PathNotFoundException, rethrow with the paths
			// full name.
		} catch (PathNotFoundException e) {
			throw new PathNotFoundException(path);
		}

	}

	/**
	 * Recursively find the {@link Item} within the {@code currentDir} given its
	 * path split into an {@link ArrayList} at every '/'
	 * 
	 * @param paths
	 *            Location of the {@link item}.
	 * @return The item located at {@code path}
	 * @throws PathNotFoundException
	 *             Thrown when the item could not be found at the path.
	 */
	private Item findItemByPath(Directory currentDir, ArrayList<String> paths)
			throws PathNotFoundException {
		if (paths.size() > 0) {
			Item item;
			if (paths.get(0).equals("."))
				item = currentDir;
			else if (paths.get(0).equals(".."))
				item = currentDir.getParent();
			else
				item = currentDir.getChild(paths.get(0));

			if (item != null) {
				// Remove the Item found above from the ArrayList
				paths.remove(0);
				// If there are no more Items to find
				if (paths.size() == 0)
					return item;
				// If the item is a directory, recurse through the remaining
				// path
				else if (item instanceof Directory)
					return findItemByPath((Directory) item, paths);
				// Otherwise throw an error because the item found was a File
				else
					throw new PathNotFoundException();
			}
			// If the child does not exist in the parent, throw an error.
			else
				throw new PathNotFoundException();
		} else
			return currentDir;
	}

	public void get(Directory currentDir, String URL)
			throws InvalidPathException, PathNotFoundException {

		// testing address:
		// http://www.cs.cmu.edu/~spok/grimmtmp/073.txt
		// http://www.ub.edu/gilcub/SIMPLE/simple.html(this link doesn't work)
		// http://www.cs.toronto.edu/~liudavid/csc236/home.html (CSC236 webpage)

		URL link;
		System.out.print("Reading " + URL);
		int idx = URL.lastIndexOf("/");
		String filename = (String) URL.subSequence((idx + 1), URL.length());
		System.out.println("\nGet new file: " + filename);
		try {
			link = new URL(URL);
			URLConnection conn = link.openConnection();
			BufferedReader br = new BufferedReader(new InputStreamReader(
					conn.getInputStream()));
			String inputLine;

			while ((inputLine = br.readLine()) != null) {
				// testing :
				// System.out.println(inputLine);
				echo(currentDir, (inputLine + "\n"), filename, false);
			}
			br.close();
			System.out.println("\nDone");
		} catch (MalformedURLException e) {
			throw new InvalidPathException(URL);
		} catch (IOException e) {
			throw new InvalidPathException(URL);
		}

	}

}
